function defineReactive(obj, key, val) {
    observe(val)

    const dep = new Dep();
    Object.defineProperty(obj, key, {
        get() {
            console.log(`get ${key}, ${val}`);

            Dep.target && dep.addDep(Dep.target)
            return val;
        },
        set(newVal) {
            if (val !== newVal) {
                console.log(`set ${key}, ${newVal}`);
                observe(newVal)
                val = newVal;

                // watchers.forEach(w => w.update())
                dep.notify()
            }
        }
    })
}

function observe(obj) {
    if (typeof obj !== 'object' || obj == null) {
        return
    } 

    new Observer(obj)
}

class Observer {
    constructor(value) {
        if (Array.isArray(value)) {
            // todo
        } else {
            this.walk(value)
        }
    }

    walk(obj) {
        Object.keys(obj).forEach(key => {
            defineReactive(obj, key, obj[key])
        })
    }
}

function set(obj, key, val) {
    defineReactive(obj, key, val)
}

// const obj = {}
// observe(obj)
// obj.foo = "fooolll"
// obj.foo
// console.log("demo done")

function proxy(vm, key) {
    Object.keys(vm[key]).forEach(k => {
        Object.defineProperty(vm, k, {
            get() {
                return vm[key][k]
            },
            set(v) {
                vm[key][k] = v
            }
        })
    })
}

class KVue {
    constructor(options) {
        this.$options = options
        this.$data = options.data

        // make the data reactive
        observe(this.$data)

        proxy(this, "$data")

        // new Compile(options.el, this)
        if (options.el) {
            this.$mount(options.el)
        }
    }

    $mount(el) {
        this.$el = document.querySelector(el)

        const updateComponent = () => {
            const {render} = this.$options;
            // const el = render.call(this);
            // const parent = this.$el.parentElement
            // parent.insertBefore(el, this.$el.nextSibling)
            // parent.removeChild(this.$el)
            // this.$el = el
            const vnode = render.call(this, this.$createElement)
            this._update(vnode)
            
        }

        new Watcher(this, updateComponent)
    }

    $createElement(tag, props, children) {
        return {
            tag,
            props,
            children
        }
    }

    _update(vnode) {
        const prevVNode = this._vnode
        if (!prevVNode){
            //init
            this.__patch__(this.$el, vnode) 
        } else {
            this.__patch__(prevVNode, vnode)
        }
    }

    __patch__(oldVNode, vnode) {
        if (oldVNode.nodeType) {
            const parent = oldVNode.parentElement
            const refElm = oldVNode.nextSibling
            const el = this.createElm(vnode)

            parent.insertBefore(el, refElm)
            parent.removeChild(oldVNode)

            this._vnode = vnode
        } else {
            // update
            const el = vnode.el = oldVNode.el
            if (oldVNode.tag === vnode.tag) {
                //props 
                const oldProps = oldVNode.props || {}
                const newProps = vnode.props || {}
                for (const key in newProps) {
                    const oldValue = oldProps[key]
                    const newValue = newProps[key]

                    if (oldValue !== newValue) {
                        el.setAttribute(key, newValue)
                    }
                }

                for (const key in oldProps) {
                    if (!(key in newProps)) {
                    // if (!newProps.hasOwnProperty(key)) {
                        el.removeAttribute(key)
                    }
                }
                
                //children
                const oldCh = oldVNode.children
                const newCh = vnode.children

                if (typeof newCh === 'string') {
                    if (typeof oldCh === 'string') {
                        if (oldCh !== newCh) {
                            el.textContent = newCh
                        } 
                    } else {
                        // there is no text before
                        el.textContent = newCh
                    }
                }else {
                    if (typeof oldCh === 'string') {
                        el.innerHTML = ""
                        newCh.forEach(child => this.createElm(child))
                    } else {
                        this.updateChildren(el, oldCh, newCh);
                    }
                }
            }

        }
    }

    createElm(vnode) {
        const el = document.createElement(vnode.tag)

        if (vnode.props) {
            for(const key in vnode.props) {
                const value = vnode.props[key]
                el.setAttribute(key, value)
            }
        }

        if (vnode.children) {
            if (typeof vnode.children === 'string') {
                el.textContent = vnode.children
            } else {
                vnode.children.forEach(n => {
                    const child = this.createElm(n)
                    el.appendChild(child)
                })
            }
        }

        vnode.el = el
        return el;
    }

    updateChildren(parentElm, oldCh, newCh) {
        const len = Math.min(oldCh.length, newCh.length)
        for (let i = 0; i < len; i++) {
            this.__patch__(oldCh[i], newCh[i])
        }

        if (newCh.length > oldCh.length) {
            // Add node
            newCh.slice(len).forEach(child => {
                const el = this.createElm(child)
                parentElm.appendChild(el)
            })
        } else if (newCh.length < oldCh.length) {
            // remove
            oldCh.slice(len).forEach(child => {
                const el = this.createElm(child)
                parentElm.removeChild(el)
            })
        }
    }
}


class Compile {
    constructor(el, vm) {
        this.$vm = vm
        this.$el = document.querySelector(el)

        if (this.$el) {
            this.compile(this.$el)
        }
    }

    compile(el) {
        const childNodes = el.childNodes

        childNodes.forEach(node => {
            if (node.nodeType === 1) {
                // element
                const attrs = node.attributes
                Array.from(attrs).forEach(attr => {
                    const attrName = attr.name
                    const exp = attr.value
                    if (attrName.startsWith("k-")) {
                        const dir = attrName.substring(2)
                        this[dir] && this[dir](node, exp)
                    }else if (attrName.startsWith("@")) {
                        const dir = attrName.substring(1)
                        this[dir] && this[dir](node, exp)
                    }else if (attrName === "v-model") {
                        const dir = "input_model"
                        this[dir] && this[dir](node, exp)
                    }

                })
            } else if (this.isInter(node)) {
                // text
                console.log("inter", node.textContent)
                this.compileText(node)
            }

            if (node.childNodes && node.childNodes.length > 0) {
                this.compile(node)
            }
        })
    }

    isInter(node) {
        return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
    }

    compileText(node) {
        // node.textContent = this.$vm[RegExp.$1]
        this.update(node, RegExp.$1, 'text')
    }

    update(node, exp, directive) {
        // 1. init 
        const fn = this[directive + 'Updater']
        fn && fn(node, this.$vm[exp]) 
        
        // 2. update
        new Watcher(this.$vm, exp, function(val) {
            fn && fn(node, val)
        })
    }

    text(node, exp) {
        this.update(node, exp, 'text')
    }

    textUpdater(node, value) {
        node.textContent = value
    }

    click(node, exp) {
        console.log("click is called. to run exp");
        node.addEventListener('click', this.$vm.$options[exp])
    }

    input_model(node, exp) {
        const newVal = this.$vm.$data[exp]
        node.value = newVal
        set(this.$vm.$data, 'input_data', newVal)
        const vm = this.$vm
        node.addEventListener('keyup', function(e) {
            console.log(e)
            console.log("data has been changed")
            vm.$data[exp] = e.target.value
        }, false)
    }

}


const watchers = []

class Watcher {
    constructor(vm, fn) {
        this.vm = vm;
        this.getter = fn;

        this.get()
        // watchers.push(this);
    }

    get() {
        Dep.target = this
        this.getter.call(this.vm)
        Dep.target = null
    }
    update() {
        this.get()
    }
}


class Dep {
    constructor() {
        this.deps = new Set();
    }

    addDep(dep) {
        this.deps.add(dep)
    }

    notify() {
        this.deps.forEach(watcher => watcher.update())
    }
}